<template>
  <view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
    <canvas
      v-if="use2d"
      type="2d"
      id="imgCanvas"
      class="img-canvas"
      :style="{
        width: `${canvansWidth}px`,
        height: `${canvansHeight}px`,
      }"
    ></canvas>
    <canvas
      v-else
      id="imgCanvas"
      canvas-id="imgCanvas"
      class="img-canvas"
      :style="{
        width: `${canvansWidth}px`,
        height: `${canvansHeight}px`,
      }"
    ></canvas>
    <view
      id="pic-preview"
      class="pic-preview"
      :change:init="cropper.initObserver"
      :init="initData"
      @touchstart="cropper.touchstart"
      @touchmove="cropper.touchmove"
      @touchend="cropper.touchend"
    >
      <image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
      <view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
      <view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
      <view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
        <view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
      </view>
      <block v-if="showGrid">
        <view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
      </block>
      <block v-if="showAngle">
        <view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
          <view
            :style="[
              {
                width: `${angleSize}px`,
                height: `${angleSize}px`,
              },
            ]"
          ></view>
        </view>
      </block>
    </view>
    <slot />
    <view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
      <view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
        <view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
        <view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
      </view>
      <view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
      <block v-else-if="!!imgSrc">
        <view class="rechoose" @click="chooseImage">重选</view>
        <button class="button" size="mini" @click="cropClick">确定</button>
      </block>
      <view v-else class="choose-btn" @click="chooseImage">选择图片</view>
    </view>
  </view>
</template>

<!-- #ifdef APP-VUE -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
// vue3 app renderjs中条件编译无效
cropper.setPlatform('APP');
export default {
	mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef H5 -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
export default {
	mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-QQ -->
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
<!-- #endif -->
<script>
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
const AREA_SIZE = 75
/** 图片默认宽高 */
const IMG_SIZE = 200
import { syncUniApi } from '@/common/utils/utils.js'
export default {
  name: 'qf-image-cropper',
  // #ifdef MP-WEIXIN
  options: {
    // 表示启用样式隔离，在自定义组件内外，使用 class 指定的样式将不会相互影响
    styleIsolation: 'isolated',
  },
  // #endif
  props: {
    isShow: {
      type: Boolean,
      default: false,
    },
    /** 图片资源地址 */
    src: {
      type: String,
      default: '',
    },
    /** 裁剪宽度，有些平台或设备对于canvas的尺寸有限制，过大可能会导致无法正常绘制 */
    width: {
      type: [Number, String],
      default: IMG_SIZE,
    },
    /** 裁剪高度，有些平台或设备对于canvas的尺寸有限制，过大可能会导致无法正常绘制 */
    height: {
      type: [Number, String],
      default: IMG_SIZE,
    },
    /** 是否绘制裁剪区域边框 */
    showBorder: {
      type: Boolean,
      default: true,
    },
    /** 是否绘制裁剪区域网格参考线 */
    showGrid: {
      type: Boolean,
      default: true,
    },
    /** 是否展示四个支持伸缩的角 */
    showAngle: {
      type: Boolean,
      default: true,
    },
    /** 裁剪区域最小缩放倍数 */
    areaScale: {
      type: Number,
      default: 0.3,
    },
    /** 图片最小缩放倍数 */
    minScale: {
      type: Number,
      default: 1,
    },
    /** 图片最大缩放倍数 */
    maxScale: {
      type: Number,
      default: 5,
    },
    /** 检查图片位置是否超出裁剪边界，如果超出则会矫正位置 */
    checkRange: {
      type: Boolean,
      default: true,
    },
    /** 生成图片背景色：如果裁剪区域没有完全包含在图片中时，不设置该属性生成图片存在一定的透明块 */
    backgroundColor: {
      type: String,
    },
    /** 是否有回弹效果：当 checkRange 为 true 时有效，拖动时可以拖出边界，释放时会弹回边界 */
    bounce: {
      type: Boolean,
      default: true,
    },
    /** 是否支持翻转 */
    rotatable: {
      type: Boolean,
      default: true,
    },
    /** 是否支持逆向翻转 */
    reverseRotatable: {
      type: Boolean,
      default: false,
    },
    /** 是否支持从本地选择素材 */
    choosable: {
      type: Boolean,
      default: true,
    },
    /** 是否开启硬件加速，图片缩放过程中如果出现元素的“留影”或“重影”效果，可通过该方式解决或减轻这一问题 */
    gpu: {
      type: Boolean,
      default: false,
    },
    /** 四个角尺寸，单位px */
    angleSize: {
      type: Number,
      default: 20,
    },
    /** 四个角边框宽度，单位px */
    angleBorderWidth: {
      type: Number,
      default: 2,
    },
    zIndex: {
      type: [Number, String],
    },
    /** 裁剪图片圆角半径，单位px */
    radius: {
      type: Number,
      default: 0,
    },
    /** 生成文件的类型，只支持 'jpg' 或 'png'。默认为 'png' */
    fileType: {
      type: String,
      default: 'png',
    },
    /**
     * 图片从绘制到生成所需时间，单位ms
     * 微信小程序平台使用 `Canvas 2D` 绘制时有效
     * 如绘制大图或出现裁剪图片空白等情况应适当调大该值，因 `Canvas 2d` 采用同步绘制，需自己把控绘制完成时间
     */
    delay: {
      type: Number,
      default: 1000,
    },
    // #ifdef H5
    /**
     * 页面是否是原生标题栏
     * H5平台当 showAngle 为 true 时，使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时，必须将此值设为 false ，否则四个可拉伸角的触发位置会有偏差。
     * 注：因H5平台的窗口高度是包含标题栏的，而屏幕触摸点的坐标是不包含的
     */
    navigation: {
      type: Boolean,
      default: true,
    },
    // #endif
  },
  emits: ['crop'],
  data() {
    return {
      // 用不同 id 使 v-for key 不重复
      maskList: [{ id: 'crop-mask-block-1' }, { id: 'crop-mask-block-2' }, { id: 'crop-mask-block-3' }, { id: 'crop-mask-block-4' }],
      gridList: [{ id: 'crop-grid-1' }, { id: 'crop-grid-2' }, { id: 'crop-grid-3' }, { id: 'crop-grid-4' }],
      angleList: [{ id: 'crop-angle-1' }, { id: 'crop-angle-2' }, { id: 'crop-angle-3' }, { id: 'crop-angle-4' }],
      /** 本地缓存的图片路径 */
      imgSrc: '',
      /** 图片的裁剪宽度 */
      imgWidth: IMG_SIZE,
      /** 图片的裁剪高度 */
      imgHeight: IMG_SIZE,
      /** 裁剪区域最大宽度所占屏幕宽度百分比 */
      widthPercent: AREA_SIZE,
      /** 裁剪区域最大高度所占屏幕宽度百分比 */
      heightPercent: AREA_SIZE,
      /** 裁剪区域布局信息 */
      area: {},
      /** 未被缩放过的图片宽 */
      oldWidth: 0,
      /** 未被缩放过的图片高 */
      oldHeight: 0,
      /** 系统信息 */
      sys: uni.getSystemInfoSync(),
      scaleWidth: 0,
      scaleHeight: 0,
      rotate: 0,
      offsetX: 0,
      offsetY: 0,
      use2d: false,
      canvansWidth: 0,
      canvansHeight: 0,
      // imageStyles: {},
      // maskStylesList: [{}, {}, {}, {}],
      // borderStyles: {},
      // gridStylesList: [{}, {}, {}, {}],
      // angleStylesList: [{}, {}, {}, {}],
      // circleBoxStyles: {},
      // circleStyles: {},
    }
  },
  computed: {
    initData() {
      // console.log('initData')
      return {
        timestamp: new Date().getTime(),
        area: {
          ...this.area,
          bounce: this.bounce,
          showBorder: this.showBorder,
          showGrid: this.showGrid,
          showAngle: this.showAngle,
          angleSize: this.angleSize,
          angleBorderWidth: this.angleBorderWidth,
          minScale: this.areaScale,
          widthPercent: this.widthPercent,
          heightPercent: this.heightPercent,
          radius: this.radius,
          checkRange: this.checkRange,
          zIndex: +this.zIndex || 0,
        },
        sys: this.sys,
        img: {
          minScale: this.minScale,
          maxScale: this.maxScale,
          src: this.imgSrc,
          width: this.oldWidth,
          height: this.oldHeight,
          oldWidth: this.oldWidth,
          oldHeight: this.oldHeight,
          gpu: this.gpu,
        },
      }
    },
    imgProps() {
      return {
        width: this.width,
        height: this.height,
        src: this.src,
      }
    },
    display() {
      return this.isShow ? 'block' : 'none'
    },
  },
  watch: {
    imgProps: {
      handler(val, oldVal) {
        // 自定义裁剪尺，示例如下：
        this.imgWidth = Number(val.width) || IMG_SIZE
        this.imgHeight = Number(val.height) || IMG_SIZE
        let use2d = true
        // #ifndef MP-WEIXIN
        use2d = false
        // #endif
        // if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
        // 	use2d = false;
        // }
        let canvansWidth = this.imgWidth
        let canvansHeight = this.imgHeight
        let size = Math.max(canvansWidth, canvansHeight)
        let scalc = 1
        if (size > 1365) {
          scalc = 1365 / size
        }
        this.canvansWidth = canvansWidth * scalc
        this.canvansHeight = canvansHeight * scalc
        this.use2d = use2d
        this.initArea()
        const src = val.src || this.imgSrc
        src && this.initImage(src, oldVal === undefined)
      },
      immediate: true,
    },
    isShow(newVal) {
      return newVal ? 'block' : 'none'
    },
  },
  methods: {
    /** 提供给wxs调用，用来接收图片变更数据 */
    dataChange(e) {
      // console.log('dataChange', e)
      this.scaleWidth = e.width
      this.scaleHeight = e.height
      this.rotate = e.rotate
      this.offsetX = e.x
      this.offsetY = e.y
    },
    /** 初始化裁剪区域布局信息 */
    initArea() {
      // 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
      this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom
      // #ifndef H5
      this.sys.windowTop = 0
      this.sys.navigation = true
      // #endif
      // #ifdef H5
      // h5平台的窗口高度是包含标题栏的
      this.sys.windowTop = this.sys.windowTop || 44
      this.sys.navigation = this.navigation
      // #endif
      let wp = this.widthPercent
      let hp = this.heightPercent
      if (this.imgWidth > this.imgHeight) {
        hp = (hp * this.imgHeight) / this.imgWidth
      } else if (this.imgWidth < this.imgHeight) {
        wp = (wp * this.imgWidth) / this.imgHeight
      }
      const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth
      const width = (size * wp) / 100
      const height = (size * hp) / 100
      const left = (this.sys.windowWidth - width) / 2
      const right = left + width
      const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2
      const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top
      this.area = { width, height, left, right, top, bottom }
      this.scaleWidth = width
      this.scaleHeight = height
    },
    /** 从本地选取图片 */
    async chooseImage(options) {
      // #ifdef MP-WEIXIN || MP-JD
      if (uni.chooseMedia) {
        uni.chooseMedia({
          ...options,
          count: 1,
          mediaType: ['image'],
          success: (res) => {
            this.resetData()
            this.initImage(res.tempFiles[0].tempFilePath)
          },
        })
        return
      }
      // #endif

      /** 使用syncUniApi()方法，每次调用前先检测权限 */
      let res = await syncUniApi('chooseImage', {
        ...options,
        count: 1, // 默认3
        sourceType: ['album', 'camera'],
        sizeType: ['original', 'compressed'],
      })
      this.initImage(res.tempFiles[0].path)
      this.resetData()
    },
    /** 重置数据 */
    resetData() {
      this.imgSrc = ''
      this.rotate = 0
      this.offsetX = 0
      this.offsetY = 0
      this.initArea()
    },
    /**
     * 初始化图片信息
     * @param {String} url 图片链接
     */
    initImage(url, isFirst) {
      uni.getImageInfo({
        src: url,
        success: async (res) => {
          if (isFirst && this.src === url) await new Promise((resolve) => setTimeout(resolve, 50))
          this.imgSrc = res.path
          let scale = res.width / res.height
          let areaScale = this.area.width / this.area.height
          if (scale > 1) {
            // 横向图片
            if (scale >= areaScale) {
              // 图片宽不小于目标宽，则高固定，宽自适应
              this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth)
            } else {
              // 否则宽固定、高自适应
              this.scaleHeight = (res.height * this.scaleWidth) / res.width
            }
          } else {
            // 纵向图片
            if (scale <= areaScale) {
              // 图片高不小于目标高，宽固定，高自适应
              this.scaleHeight = ((this.scaleWidth / res.width) * this.scaleHeight) / (this.scaleHeight / res.height)
            } else {
              // 否则高固定，宽自适应
              this.scaleWidth = (res.width * this.scaleHeight) / res.height
            }
          }
          // 记录原始宽高，为缩放比列做限制
          this.oldWidth = this.scaleWidth
          this.oldHeight = this.scaleHeight
        },
        fail: (err) => {
          console.error(err)
        },
      })
    },
    /**
     * 剪切图片圆角
     * @param {Object} ctx canvas 的绘图上下文对象
     * @param {Number} radius 圆角半径
     * @param {Number} scale 生成图片的实际尺寸与截取区域比
     * @param {Function} drawImage 执行剪切时所调用的绘图方法，入参为是否执行了剪切
     */
    drawClipImage(ctx, radius, scale, drawImage) {
      if (radius > 0) {
        ctx.save()
        ctx.beginPath()
        const w = this.canvansWidth
        const h = this.canvansHeight
        if (w === h && radius >= w / 2) {
          // 圆形
          ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI)
        } else {
          // 圆角矩形
          if (w !== h) {
            // 限制圆角半径不能超过短边的一半
            radius = Math.min(w / 2, h / 2, radius)
            // radius = Math.min(Math.max(w, h) / 2, radius);
          }
          ctx.moveTo(radius, 0)
          ctx.arcTo(w, 0, w, h, radius)
          ctx.arcTo(w, h, 0, h, radius)
          ctx.arcTo(0, h, 0, 0, radius)
          ctx.arcTo(0, 0, w, 0, radius)
          ctx.closePath()
        }
        ctx.clip()
        drawImage && drawImage(true)
        ctx.restore()
      } else {
        drawImage && drawImage(false)
      }
    },
    /**
     * 旋转图片
     * @param {Object} ctx canvas 的绘图上下文对象
     * @param {Number} rotate 旋转角度
     * @param {Number} scale 生成图片的实际尺寸与截取区域比
     */
    drawRotateImage(ctx, rotate, scale) {
      if (rotate !== 0) {
        // 1. 以图片中心点为旋转中心点
        const x = (this.scaleWidth * scale) / 2
        const y = (this.scaleHeight * scale) / 2
        ctx.translate(x, y)
        // 2. 旋转画布
        ctx.rotate((rotate * Math.PI) / 180)
        // 3. 旋转完画布后恢复设置旋转中心时所做的偏移
        ctx.translate(-x, -y)
      }
    },
    drawImage(ctx, image, callback) {
      // 生成图片的实际尺寸与截取区域比
      const scale = this.canvansWidth / this.area.width
      if (this.backgroundColor) {
        if (ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor)
        else ctx.fillStyle = this.backgroundColor
        ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight)
      }
      this.drawClipImage(ctx, this.radius, scale, () => {
        this.drawRotateImage(ctx, this.rotate, scale)
        const r = this.rotate / 90
        ctx.drawImage(
          image,
          [this.offsetX - this.area.left, this.offsetY - this.area.top, -(this.offsetX - this.area.left), -(this.offsetY - this.area.top)][r] * scale,
          [this.offsetY - this.area.top, -(this.offsetX - this.area.left), -(this.offsetY - this.area.top), this.offsetX - this.area.left][r] * scale,
          this.scaleWidth * scale,
          this.scaleHeight * scale
        )
      })
    },
    /**
     * 绘图
     * @param {Object} canvas
     * @param {Object} ctx canvas 的绘图上下文对象
     * @param {String} src 图片路径
     * @param {Function} callback 开始绘制时回调
     */
    draw2DImage(canvas, ctx, src, callback) {
      // console.log('draw2DImage', canvas, ctx, src, callback)
      if (canvas) {
        const image = canvas.createImage()
        image.onload = () => {
          this.drawImage(ctx, image)
          // 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
          callback && setTimeout(callback, this.delay)
        }
        image.onerror = (err) => {
          console.error(err)
          uni.hideLoading()
        }
        image.src = src
      } else {
        this.drawImage(ctx, src)
        setTimeout(() => {
          ctx.draw(false, callback)
        }, 200)
      }
    },
    /**
     * 画布转图片到本地缓存
     * @param {Object} canvas
     * @param {String} canvasId
     */
    canvasToTempFilePath(canvas, canvasId) {
      // console.log('canvasToTempFilePath', canvas, canvasId)
      uni.canvasToTempFilePath(
        {
          canvas,
          canvasId,
          x: 0,
          y: 0,
          width: this.canvansWidth,
          height: this.canvansHeight,
          destWidth: this.imgWidth, // 必要，保证生成图片宽度不受设备分辨率影响
          destHeight: this.imgHeight, // 必要，保证生成图片高度不受设备分辨率影响
          fileType: this.fileType, // 目标文件的类型，默认png
          success: (res) => {
            // 生成的图片临时文件路径
            this.handleImage(res.tempFilePath)
          },
          fail: (err) => {
            uni.hideLoading()
            uni.showToast({ title: '裁剪失败，生成图片异常！', icon: 'none' })
          },
        },
        this
      )
    },
    /** 确认裁剪 */
    cropClick() {
      uni.showLoading({ title: '裁剪中...', mask: true })
      if (!this.use2d) {
        const ctx = uni.createCanvasContext('imgCanvas', this)
        ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight)
        this.draw2DImage(null, ctx, this.imgSrc, () => {
          this.canvasToTempFilePath(null, 'imgCanvas')
        })
        return
      }
      // #ifdef MP-WEIXIN
      const query = uni.createSelectorQuery().in(this)
      query
        .select('#imgCanvas')
        .fields({ node: true, size: true })
        .exec((res) => {
          const canvas = res[0].node

          const dpr = uni.getSystemInfoSync().pixelRatio
          canvas.width = res[0].width * dpr
          canvas.height = res[0].height * dpr
          const ctx = canvas.getContext('2d')
          ctx.scale(dpr, dpr)
          ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight)

          this.draw2DImage(canvas, ctx, this.imgSrc, () => {
            this.canvasToTempFilePath(canvas)
          })
        })
      // #endif
    },
    handleImage(tempFilePath) {
      // 在H5平台下，tempFilePath 为 base64
      // console.log(tempFilePath)
      uni.hideLoading()
      this.$emit('crop', { tempFilePath })
    },
  },
}
</script>

<style lang="scss" scoped>
.image-cropper {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background-color: #000;
  .img-canvas {
    position: absolute !important;
    transform: translateX(-100%);
  }
  .pic-preview {
    width: 100%;
    flex: 1;
    position: relative;

    .crop-mask-block {
      background-color: rgba(51, 51, 51, 0.8);
      z-index: 2;
      position: fixed;
      box-sizing: border-box;
      pointer-events: none;
    }
    .crop-circle-box {
      position: fixed;
      box-sizing: border-box;
      z-index: 2;
      pointer-events: none;
      overflow: hidden;
      .crop-circle {
        width: 100%;
        height: 100%;
      }
    }
    .crop-image {
      padding: 0 !important;
      margin: 0 !important;
      border-radius: 0 !important;
      display: block !important;
      backface-visibility: hidden;
    }
    .crop-border {
      position: fixed;
      border: 1px solid #fff;
      box-sizing: border-box;
      z-index: 3;
      pointer-events: none;
    }
    .crop-grid {
      position: fixed;
      z-index: 3;
      border-style: dashed;
      border-color: #fff;
      pointer-events: none;
      opacity: 0.5;
    }
    .crop-angle {
      position: fixed;
      z-index: 3;
      border-style: solid;
      border-color: #fff;
      pointer-events: none;
    }
  }

  .fixed-bottom {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 99;
    display: flex;
    flex-direction: row;
    background-color: $uni-bg-color-grey;

    .action-bar {
      position: absolute;
      top: -90rpx;
      left: 10rpx;
      display: flex;
      .rotate-icon {
        background-image: url('');
        background-size: 60% 60%;
        background-repeat: no-repeat;
        background-position: center;
        width: 80rpx;
        height: 80rpx;
        &.is-reverse {
          transform: rotateY(180deg);
        }
      }
    }

    .rechoose {
      color: $uni-color-primary;
      padding: 0 $uni-spacing-row-lg;
      line-height: 100rpx;
    }

    .choose-btn {
      color: $uni-color-primary;
      text-align: center;
      line-height: 100rpx;
      flex: 1;
    }

    .button {
      margin: auto $uni-spacing-row-lg auto auto;
      background-color: $uni-color-primary;
      color: #fff;
    }
  }

  .safe-area-inset-bottom {
    padding-bottom: 0;
    padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
    padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
  }
}
</style>
